#pragma once

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <stdexcept>
#include <tuple>
#include <cstring>
#include <cstdlib>
#include <cstdint>
#if defined (__linux__) || defined (__APPLE__)
#include <unistd.h>
#include <limits.h>
#include <dirent.h>
#elif defined (_WIN32)  || defined (_WIN64)
#include <windows.h>
#include <direct.h>
#endif
#include <sys/stat.h>

namespace easyfs
{
    struct path_sepr;
    using string_type = std::string;
    using c_str_type = const char*;
    using int64_type = uint64_t;
    using time_type = time_t;
    using sepr_type = char;

    struct path_sepr
    {
        enum seprs
        {
            win = '\\',
            unix = '/',
#if defined (_WIN32)  || defined (_WIN64)
            sepr = win,
#elif defined (__linux__) || defined (__APPLE__)
            sepr = unix,
#endif
        };

        static bool is_sepr(const sepr_type sepr) { return sepr == win || sepr == unix; }
    };


    class path;
    extern path absolute(const path&);
    extern path relative(const path&);

    class path
    {
    public:
#if defined (EASYFS_IGNORE_PATH_STREAM_easyfs)
        friend std::ostream& operator<<(std::ostream& os, const path& p);
        friend std::istream& operator>>(std::istream& is, path& p);
#endif

    public:
        path() : _p("") {}

        path(c_str_type p) : _p(p) {}

        path(const string_type& p) : _p(p) {}

        path(const path& p) 
        {
            if (&p != this) _p = p._p;
        }

        path& operator=(const path& p)
        {
            if (&p != this) _p = p._p;
            return *this;
        }

        path& assign(const string_type& s)
        {
            return *this = s;
        }

        path operator+(const path& p) const 
        {
            return _p + p._p;
        }
        path& operator+=(const path& p)
        {
            _p += p._p;
            return *this;
        }

        path operator/(const path& p) const 
        {
            path tmp(*this);
            return tmp /= p;
        }
        path& operator/=(const path& p)
        {
            if (path_sepr::is_sepr(_p.back()))
                _p += p._p;
            else
                _p += (char)path_sepr::seprs::sepr + p._p;
            return *this;
        }

        path& append(const path& p) { return *this += p; }

        void clear() { _p.clear(); }

        size_t size() const { return _p.size(); }

        path& change_sepr(const sepr_type sepr)
        {
            sepr_type old_sepr = 
                (sepr == path_sepr::unix ? path_sepr::win : path_sepr::unix);

            size_t pos = _p.find(old_sepr); 
            while (pos != string_type::npos)
            {
                _p[pos] = sepr;
                pos = _p.find(old_sepr); 
            }
            return *this;
        }

        path& remove_filename()
        {
            size_t pos = _p.rfind(path_sepr::seprs::sepr);
            _p.erase(pos + 1);
            return *this;
        }

        path& remove_top_directory()
        {
            size_t pos = _p.find(path_sepr::seprs::sepr);
            _p = _p.substr(pos + 1);
            return *this;
        }

        path& replace_top_directory(const path& p)
        {
            remove_top_directory();
            *this = p / _p;
            return *this;
        }

        path& replace_filename(const path& replace)
        {
            remove_filename();
            _p += replace._p;
            return *this;
        }

        path& replace_extension(const path& replace)
        {
            size_t pos = _p.rfind('.');
            _p.erase(pos);
            _p += replace._p;
            return *this;
        }

        const char* c_str() const { return _p.c_str(); }

        string_type& string() { return _p; }
        const string_type& string() const { return _p; }

        path absolute_path() const { return absolute(_p); }
        path relative_path() const { return relative(_p); }

        // get the parent directory
        const path parent_path() const 
        {
            const auto& res = split(path_sepr::seprs::sepr);

            if (std::get<2>(res) == true)
                return std::get<0>(res);
            else
                return ".";
        }

        // get the filename and extension
        path filename() const 
        {
            const auto& res = split(path_sepr::seprs::sepr);

            if (std::get<2>(res) == true)
                return std::get<1>(res);
            else
                return *this;
        }

        // get the filename but no extension
        path stem() const 
        {
            return std::get<0>(filename().split('.'));
        }

        // get the extension
        path extension() const 
        {
            return '.' + std::get<1>(filename().split('.'));
        }

        bool operator==(const path& p) const { return _p == p._p; }
        bool operator!=(const path& p) const { return _p != p._p; }
        bool operator< (const path& p) const { return _p <  p._p; }
        bool operator<=(const path& p) const { return _p <= p._p; }
        bool operator> (const path& p) const { return _p >  p._p; }
        bool operator>=(const path& p) const { return _p >= p._p; }

    private:
        std::tuple<string_type, string_type, bool> split(const sepr_type sepr) const
        {
            return split(_p, sepr);
        }
        std::tuple<string_type, string_type, bool> split(const path& p, const sepr_type sepr) const
        {
            return split(p._p, sepr);
        }
        std::tuple<string_type, string_type, bool> split(const string_type& s, const sepr_type sepr) const
        {
            size_t pos = _p.rfind(sepr);

            if (pos == string_type::npos)
                return { "", "", false };
            else
                return { _p.substr(0, pos), _p.substr(pos + 1), true };
        }

    private:
        string_type _p;
    };

#if defined (EASYFS_IGNORE_PATH_STREAM_easyfs)
    inline std::ostream& operator<<(std::ostream& os, const path& p)
    {
        os << p._p;
        return os;
    }
    inline std::istream& operator>>(std::istream& is, path& p)
    {
        is >> p._p;
        return is;
    }
#endif

    inline path current_path()
    {
#if defined (_WIN32)  || defined (_WIN64)
        char tmp[MAX_PATH] = { 0 };
        if (GetCurrentDirectoryA(sizeof(tmp), tmp) == 0)
#else
        char tmp[PATH_MAX] = { 0 };
        if (getcwd(tmp, sizeof(tmp)) == nullptr)
#endif
            throw std::runtime_error("easyfs path error: relative_path");

        return tmp;
    }

    inline void current_path(const path& p)
    {
#if defined (_WIN32)  || defined (_WIN64)
        if (!SetCurrentDirectoryA(p.c_str()))
#else
        if (chdir(p.c_str()) != 0)
#endif
            throw std::runtime_error("easyfs current_path error");
    }

    inline path absolute(const path& rp)
    {
#if defined (__linux__) || defined (__APPLE__)
        char path[PATH_MAX] = { 0 };
        if (!realpath(rp.c_str(), path))
#elif defined (_WIN32)  || defined (_WIN64)
        char path[MAX_PATH] = { 0 };
        if (GetFullPathNameA(rp.c_str(), sizeof(path), path, nullptr) == 0)
#endif
            throw std::runtime_error("easyfs path error: absolute");
        return path;
    }

    inline path relative(const path& ap)
    {
        path cwd = current_path();

        size_t pos = ap.string().find(cwd.string());
        if (pos == string_type::npos)
            throw std::runtime_error("easyfs path error: relative");

        return ap.string().substr(pos + cwd.size()  + 1); // cwd string won't end with '/', so it needs to be +1.
    }

    inline bool exists(const path& p) 
    { 
        struct stat st;
        return stat(p.c_str(), &st) == 0;
    }

    inline void create_directory(const path& path, size_t pos = 0)
    {
        static auto makedir = [](const class path& p) 
        {
            if (exists(p))
                return;
#if defined (_WIN32)  || defined (_WIN64)
            if (_mkdir(p.c_str()) != 0)
#else
            if (mkdir(p.c_str(), 0775) != 0)
#endif
                throw std::runtime_error("easyfs create_directory error: mkdir failed");
        };

        pos = path.string().find(path_sepr::seprs::sepr, pos);

        if (pos == string_type::npos)
        {
            makedir(path);
        }
        else
        {
            makedir(path.string().substr(0, pos));
            create_directory(path, pos + 1);
        }
    }

    inline bool is_directory(const path& p)
    {
        struct stat st;
        if (stat(p.c_str(), &st) != 0) 
            throw std::runtime_error("easyfs is_directory error: file doesn't exists");
#if defined (__linux__) || defined (__APPLE__)
        return S_ISDIR(st.st_mode);
#elif defined (_WIN32)  || defined (_WIN64)
        return st.st_mode & S_IFDIR;
#endif
    }
    
    inline bool is_file(const path& p)
    {
        struct stat st;
        if (stat(p.c_str(), &st) != 0)
            throw std::runtime_error("easyfs is_file error: file doesn't exists");
#if defined (__linux__) || defined (__APPLE__)
        return S_ISREG(st.st_mode);
#elif defined (_WIN32)  || defined (_WIN64)
        return st.st_mode & S_IFREG;
#endif
    }

    // equivalent to the command "mv"
    inline void move(const path& src, const path& dst)
    {
        static auto move_file = [](const path& src, const path& dst)
        {
#if defined (_WIN32)  || defined (_WIN64)
            if (!MoveFileA(src.c_str(), dst.c_str()))
#elif defined (__linux__) || defined (__APPLE__)
            if (rename(src.c_str(), dst.c_str()) != 0)
#endif
                throw std::runtime_error("easyfs rename error");
        };

        // if dst exists, then it must be a directory, 
        // otherwise, it must be a file
        if (exists(dst)) 
        {
            move_file(src.c_str(), (dst / src.filename()).c_str());
        }
        else
        {
            if (is_directory(src))
                throw std::runtime_error("easyfs move error: error file type");
            move_file(src, dst);
        }
    }

    inline int64_type file_size(const path& p)
    {
        struct stat st;
        if (stat(p.c_str(), &st) == -1)
            throw std::runtime_error("easyfs file_size error " + p.string());
        return st.st_size;
    }

    inline string_type read_file(const path& path, size_t pos = 0, size_t len = std::string::npos)
    {
        std::ifstream ifs(path.string());
        if (!ifs.is_open())
            throw std::runtime_error("easyfs read_file error " + path.string());

        string_type res;

        if (len == std::string::npos)
            len = file_size(path);

        res.resize(len);

        ifs.seekg(pos, std::ios::beg);
        ifs.read(&res[0], len);

        return res;
    }

    inline void write_file(const path& path, const string_type& content)
    {
        std::ofstream ofs(path.string());
        if (!ofs.is_open())
            throw std::runtime_error("easyfs write_file error" + path.string());

        ofs << content;
    }

    inline void create_file(const path& path)
    {
        create_directory(path.parent_path());

        std::ofstream ofs(path.string());
        if (!ofs.is_open())
            throw std::runtime_error("easyfs create_file error" + path.string());
    }

    // if you need the prefix of the parent directory before filename,
    // you need to set the flag 'need_parent_path' to true.
    // such as: test.cc -> './test.cc'
    //          test.cc -> 'file_system/test.cc'
    inline std::vector<path> scan_directory(const path& p, bool need_parent_path = false)
    {
        if (!exists(p))
            throw std::runtime_error("easyfs scan_directory error " + p.string());

#if defined (__linux__) || defined (__APPLE__)

        struct dirent** list = nullptr;

        int n = scandir(p.c_str(), &list, [](const struct dirent* entry) ->int 
        {
            if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) return 0;
            else return 1;
        } , nullptr);

        if (n < 0)
            throw std::runtime_error("easyfs scan_directory error");

        std::vector<path> v;
        for (int i = 0; i < n; ++i)
        {
            // add the parent directory prefix to the scanned file name
            if (need_parent_path)
                v.push_back(p / path(list[i]->d_name));
            else 
                v.push_back(path(list[i]->d_name));

            free(list[i]);
        }
        free(list);

#elif defined (_WIN32)  || defined (_WIN64)
        WIN32_FIND_DATAA data;
        HANDLE handle = FindFirstFileA((p / "/*").c_str(), &data);

        if (handle == INVALID_HANDLE_VALUE)
            throw std::runtime_error("easyfs scan_directory error " + p.string());

        std::vector<path> v;
        do 
        {
            std::string file = data.cFileName;
            if (file == "." || file == "..")
                continue;

            // add the parent directory prefix to the scanned file name
            if (need_parent_path)
                v.push_back(p / path(file));
            else
                v.push_back(path(file));
        } 
        while (FindNextFileA(handle, &data));

        FindClose(handle);
#endif
        return v;
    }

    // recursive scanning of the entire directory
    inline std::vector<path> recursive_scan_directory(const path& root_path)
    {
        if (!exists(root_path))
            throw std::runtime_error("easyfs recursive_scan_directory error " + root_path.string());

        auto paths = scan_directory(root_path, true);

        // The insert inside the traversal will increase the size of paths, 
        // therefore iterators will be invalid if you choose it.
        //
        // This kind of code is not standardized enough, 
        // but I can't think of a better way just now.

        // for (auto& path : paths) // dangerous

        size_t size = paths.size();
        for (int i = 0; i < size; ++i)
        {
            const auto& path = paths[i];

            if (is_directory(path))
            {
                auto rps = recursive_scan_directory(path);
                paths.insert(paths.end(), rps.begin(), rps.end());
            }
        }

        return paths;
    }

    // remove a file or an empty directory
    inline void remove(const path& p)
    {
        if (is_file(p))
        {
            if (std::remove(p.c_str()) != 0)
                throw std::runtime_error("easyfs remove error" + p.string());
        }
        else if (is_directory(p))
        {
#if defined (_WIN32)  || defined (_WIN64)
            if (!RemoveDirectoryA(p.c_str()))
#else
            if (rmdir(p.c_str()) != 0)
#endif
                throw std::runtime_error("easyfs remove error: directory is not empty");
        }
        else
        {
            throw std::runtime_error("easyfs remove error: unknown file type");
        }
    }

    // This function is only called by other function, so you don't use it.
    // You can use remove, remove_all, clear.
    inline void recursive_remove(const path& path, bool rm_entry_dir = false)
    {
        if (is_directory(path))
        {
            auto ps = scan_directory(path, true);

            for (auto& p : ps)
            {
                recursive_remove(p, true); // each recursive call's delete flag is set to true, 
                                           // causing it only affecting the entry directory.
            }

            if (rm_entry_dir) remove(path);
        }
        else
        {
            remove(path);
        }
    }

    inline void remove_all(const path& path) { recursive_remove(path, true); }

    inline void clear(const path& path)
    {
        if (is_file(path))
        {
            create_file(path); // open a existed file will empty the file
        }
        else if (is_directory(path)) 
        {
            recursive_remove(path, false);
        }
    }

    // only copy file to file and directory to directory
    inline void copy(const path& src, const path& dst)
    {
        if (is_file(src))
        {
            create_file(dst);
            write_file(dst, read_file(src));
        }
        else if (is_directory(src))
        {
            create_directory(dst);
            std::vector<path> ps = recursive_scan_directory(src);

            create_directory(dst / src); // firstly create the root path

            for (auto p : ps)
            {
                if (is_file(p)) 
                {
                    create_file(dst / p); // create the directories are located in path.
                    copy(p, dst / p); // copy file to file
                }
                else if (is_directory(p))
                {
                    create_directory(dst / p); // copy directory to directory
                }
                else
                {
                    throw std::runtime_error("easyfs copy error: unknown file type");
                }
            }
        }
    }

    inline int64_type line_number(const path& path)
    {
        std::ifstream ifs(path.string());
        if (!ifs.is_open())
            throw std::runtime_error("easyfs line_number error " + path.string());

        int64_type cnt = 0;

        char ch = 0;
        while (ifs.get(ch))
            if (ch == '\n') ++cnt;

        return cnt;
    }

    inline time_type last_modify_time(const path& path)
    {
        struct stat st;
        if (stat(path.c_str(), &st) == -1)
            throw std::runtime_error("easyfs last_modify_time error " + path.string());
        return st.st_mtime;
    }

    inline time_type last_access_time(const path& path)
    {
        struct stat st;
        if (stat(path.c_str(), &st) == -1)
            throw std::runtime_error("easyfs last_access_time error " + path.string());
        return st.st_atime;
    }

    inline time_type last_change_time(const path& path)
    {
        struct stat st;
        if (stat(path.c_str(), &st) == -1)
            throw std::runtime_error("easyfs last_change_time error " + path.string());
        return st.st_ctime;
    }

    class file
    {
    public:
        file(const path& p) : _path(p) {}

        void create() { create_file(_path); }

        void remove() { easyfs::remove(_path); }

        void copy(const path& to) { easyfs::copy(_path, to); }

        void move(const path& to) { easyfs::move(_path, to); }

        bool exists() { return easyfs::exists(_path); }

        void clear() { easyfs::clear(_path); }

        size_t line() { return easyfs::line_number(_path); }

        int64_type size() { return easyfs::file_size(_path); }

        time_type mtime() { return easyfs::last_modify_time(_path); }

        time_type atime() { return easyfs::last_access_time(_path); }

        time_type ctime() { return easyfs::last_change_time(_path); }

        class path& path() { return _path; }

        class path dir() { return _path.parent_path(); }

        class path name() { return _path.filename(); }

        string_type read() { return easyfs::read_file(_path); }

        void write(string_type content) { easyfs::write_file(_path, content); }

    protected:
        class path _path;
    };  

    class directory
    {
    public:
        directory(const path& p) : _path(p) {}
        
        void create() { easyfs::create_directory(_path); }

        void remove() { easyfs::remove(_path); }

        void copy(const path& to) { easyfs::copy(_path, to); }

        void move(const path& to) { easyfs::move(_path, to); }

        bool exists() { return easyfs::exists(_path); }

        void clear() { easyfs::clear(_path); }

        std::vector<path> scan() { return easyfs::scan_directory(_path); }

        std::vector<path> rscan() { return easyfs::recursive_scan_directory(_path); }
            
    protected:
        path _path;
    };
}
